﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;

namespace MathService.Data
{
    /// <summary>Связь между свойствами</summary>
    /// <typeparam name="TSource">Тип объекта-источника данных</typeparam>
    /// <typeparam name="TDestination">Тип объекта-приёмника данных</typeparam>
    [Copyright("Сергей Тепляков", url = "http://www.rsdn.ru/article/dotnet/Data_Binding_Basics.xml")]
    public class PropertyLink<TSource, TDestination>
    {
        /* -------------------------------------------------------------------------------- */

        public event EventHandler EnableChanged;

        protected virtual void OnEnableChanged(EventArgs Args)
        {
            var lv_Handler = EnableChanged;
            if(lv_Handler != null) lv_Handler(this, Args);
        }

        /* -------------------------------------------------------------------------------- */

        /// <summary>
        /// Источник данных
        /// </summary>
        private readonly TSource f_Source;

        /// <summary>
        /// Приёмник данных
        /// </summary>
        private readonly TDestination f_Destination;

        /// <summary>
        /// Дескриптор свойства источника данных
        /// </summary>
        private readonly PropertyDescriptor f_SourcePropertyDescriptor;

        /// <summary>
        /// Дескриптор свойства приёмника данных
        /// </summary>
        private readonly PropertyDescriptor f_DestinationPropertyDescriptor;

        private bool f_Enable;

        /* -------------------------------------------------------------------------------- */

        /// <summary>
        /// Источник данных
        /// </summary>
        public TSource Source { get { return f_Source; } }

        /// <summary>
        /// Приёмник данных
        /// </summary>
        public TDestination Destination { get { return f_Destination; } }

        /// <summary>
        /// Дескриптор свойства источника данных
        /// </summary>
        public PropertyDescriptor SourceProperty { get { return f_SourcePropertyDescriptor; } }

        /// <summary>
        /// Дескриптор свйоства примника данных
        /// </summary>
        public PropertyDescriptor DestinationProperty { get { return f_DestinationPropertyDescriptor; } }

        /// <summary>
        /// Источник данных
        /// </summary>
        public Type SourceType { get { return typeof(TSource); } }

        /// <summary>
        /// Приёмник данных
        /// </summary>
        public Type DestinationType { get { return typeof(TDestination); } }

        /// <summary>
        /// Тип свйоства источника данных
        /// </summary>
        public Type SourcePropertyType { get { return f_SourcePropertyDescriptor.PropertyType; } }

        /// <summary>
        /// Тип свойства приёмника данных
        /// </summary>
        public Type DestinationPropertyType { get { return f_DestinationPropertyDescriptor.PropertyType; } }

        /// <summary>
        /// Активатор связи
        /// </summary>
        public bool Enable
        {
            get { return f_Enable; }
            set
            {
                if(f_Enable == value) return;
                f_Enable = value;
                OnEnableChanged(EventArgs.Empty);
            }
        }

        /* -------------------------------------------------------------------------------- */

        /// <summary>
        /// Новая связь между свойством источника и приёмника данных
        /// </summary>
        /// <param name="Source">Источник данных</param>
        /// <param name="SourcePropertyName">Имя свойства источника данных</param>
        /// <param name="Destination">Приёмник данных</param>
        /// <param name="DestinationPropertyName">Имя свойства приёмника данных</param>
        /// <param name="Enable">Признак активности связи (по умолчанию = true)</param>
        public PropertyLink(TSource Source, string SourcePropertyName,
            TDestination Destination, string DestinationPropertyName, bool Enable = true)
        {
            // ReSharper disable InvocationIsSkipped
            Contract.Requires(Source != null);
            Contract.Requires(!string.IsNullOrEmpty(SourcePropertyName));
            Contract.Requires(Destination != null);
            Contract.Requires(!string.IsNullOrEmpty(DestinationPropertyName));
            Contract.Ensures(f_SourcePropertyDescriptor != null);
            Contract.Ensures(f_DestinationPropertyDescriptor != null);
            Contract.Ensures(f_Source != null);
            Contract.Ensures(f_Destination != null);
            // ReSharper restore InvocationIsSkipped

            f_Enable = Enable;

            f_Source = Source;
            f_Destination = Destination;

            // Получаем экземпляр класса PropertyDescriptor для управления свойством элемента управления
            f_DestinationPropertyDescriptor = TypeDescriptor
                .GetProperties(Destination)
                .Find(DestinationPropertyName, true);

            if(f_DestinationPropertyDescriptor == null)
                throw new ArgumentException(string.Format("Не удалось найти свойство элемента управления с именем {0}",
                    DestinationPropertyName), nameof(DestinationPropertyName));

            //            if(f_DestinationPropertyDescriptor.SupportsChangeEvents)
            //                f_DestinationPropertyDescriptor.AddValueChanged(Destination, DestinationPropertyChanged);

            // Получаем экземпляр класса PropertyDescriptor для управления свойством источника данных
            f_SourcePropertyDescriptor = TypeDescriptor
                .GetProperties(Source)
                .Find(SourcePropertyName, true);

            if(f_SourcePropertyDescriptor == null)
                throw new ArgumentException(string.Format("Не удалось найти свойство {0} источника данных {1}",
                    SourcePropertyName, Source), nameof(SourcePropertyName));

            if(f_SourcePropertyDescriptor.SupportsChangeEvents)
                f_SourcePropertyDescriptor.AddValueChanged(Source, SourcePropertyChanged);

            // Генерация события приведет к установке значения свойства элемента управления
            if(f_Enable) SourcePropertyChanged(this, EventArgs.Empty);
        }

        /* -------------------------------------------------------------------------------- */

        /*
                /// <summary>
                /// Обработчик события изменения свойства элемента управления
                /// </summary>
                /// <param name="sender">Источник события</param>
                /// <param name="e">Аргументы события</param>
                private void DestinationPropertyChanged(object sender, EventArgs e)
                {
                    // получаем новое значение свойства элемента управления
                    var lv_ControlPropertyValue = f_DestinationPropertyDescriptor.GetValue(f_Destination);

                    // Дразу присвоить свойству источника данных новое значение нельзя, т.к. типы свойств могут не совпадать.
                    // Для этого воспользуемся TypeConverter-ами, которые являются частью класса PropertyDescriptor

                    Debug.Assert(f_SourcePropertyDescriptor.Converter != null,
                                 "f_SourcePropertyDescriptor.Converter != null");
                    Debug.Assert(f_DestinationPropertyDescriptor.Converter != null,
                        "f_DestinationPropertyDescriptor.Converter != null");

                    if(f_DestinationPropertyDescriptor.Converter.CanConvertTo(f_SourcePropertyDescriptor.PropertyType))
                    {
                        var lv_ConvertedValue = f_DestinationPropertyDescriptor.Converter.ConvertTo(
                            lv_ControlPropertyValue,
                            f_SourcePropertyDescriptor.PropertyType);
                        // изменяем значение свойства источника данных
                        f_SourcePropertyDescriptor.SetValue(f_Source, lv_ConvertedValue);
                    }
                    else
                        if(f_SourcePropertyDescriptor.Converter.CanConvertFrom(f_DestinationPropertyDescriptor.PropertyType))
                        {
                            var lv_ConvertedValue = f_SourcePropertyDescriptor.Converter.ConvertFrom(lv_ControlPropertyValue);
                            // изменяем значение свойства источника данных
                            f_SourcePropertyDescriptor.SetValue(f_Source, lv_ConvertedValue);
                        }
                        else
                            throw new Exception("Невозможно преобразование типа источника к типу свойства");

                }
        */

        /// <summary>
        /// Обработчик события изменения свойства источника данных
        /// </summary>
        /// <param name="sender">Источник события</param>
        /// <param name="e">Аргумент события</param>
        private void SourcePropertyChanged(object sender, EventArgs e)
        {
            if(!f_Enable) return;
            //получаем новое значение свойства источника данных
            var lv_DataSourceValue = f_SourcePropertyDescriptor.GetValue(f_Source);

            // сразу присвоить свойству элемента управление полученное значение 
            // нельзя, т.к. типы свойств могут не совпадать.
            // Воспользуемся TypeConverter-ами, которые являются частью класса PropertyDescriptor

            Debug.Assert(f_SourcePropertyDescriptor.Converter != null,
                         "f_SourcePropertyDescriptor.Converter != null");
            Debug.Assert(f_DestinationPropertyDescriptor.Converter != null,
                "f_DestinationPropertyDescriptor.Converter != null");

            if(f_SourcePropertyDescriptor.Converter.CanConvertTo(
                f_DestinationPropertyDescriptor.PropertyType))
            {
                var lv_ConvertedValue = f_SourcePropertyDescriptor.Converter
                    .ConvertTo(lv_DataSourceValue, f_DestinationPropertyDescriptor.PropertyType);
                // изменяем значение свойства элемента управления
                f_DestinationPropertyDescriptor.SetValue(f_Destination, lv_ConvertedValue);
            }
            else
                if(f_DestinationPropertyDescriptor.Converter.CanConvertFrom(f_SourcePropertyDescriptor.PropertyType))
                {
                    var lv_ConvertedValue = f_DestinationPropertyDescriptor.Converter.ConvertFrom(lv_DataSourceValue);
                    f_DestinationPropertyDescriptor.SetValue(f_Destination, lv_ConvertedValue);
                }
                else
                    throw new Exception("Невозможно преобразование типа источника к типу свойства");
        }

        /* -------------------------------------------------------------------------------- */

        public override string ToString()
        {
            return string.Format("{0} {1}:({2}).{3} -{8}-> {4} {5}:({6}).{7}",
                SourcePropertyType.Name, SourceType.Name, Source, f_SourcePropertyDescriptor.Name,
                DestinationPropertyType.Name, DestinationType.Name, Destination, f_DestinationPropertyDescriptor.Name,
                f_Enable ? "" : "x");
        }

        /* -------------------------------------------------------------------------------- */
    }
}
